Skip to main content

6. fine grained multithreading-Programming

6.1 Synchronization and Asynchronization in Multithreading

Parallelism refers to the simultaneous execution of two or more tasks, which can be achieved by running on multiple processors simultaneously or by interleaving execution on a single processor. In simple terms, it means doing multiple things at the same time.

Concurrency refers to multiple programs running simultaneously at a macroscopic level during a certain period of time, but only one program is executing on the processor at any given moment. In simple terms, it means multiple programs preempting CPU resources, allowing the CPU to switch rapidly, giving the illusion of simultaneous execution.

Synchronization and asynchronization in multithreading refer to the coordination and cooperation between multiple threads. In synchronous mode, multiple threads need to collaborate, and one thread's operation must wait for another thread's operation to complete before continuing. In synchronous mode, locks, condition variables, semaphores, and other mechanisms are commonly used to achieve coordination and synchronization between threads, ensuring the correct order and correctness of operations among threads. In contrast, in asynchronous mode, multiple threads can run independently without the need for coordination. One thread's operation is not affected by other threads.

For example, in a multithreaded program, multiple threads may access shared data simultaneously, requiring the use of locking mechanisms to ensure data consistency and integrity, avoiding data races and errors. Another example is the producer-consumer pattern, where producer threads and consumer threads need to cooperate to ensure the order and quantity matching of production and consumption. Condition variables and semaphores can be used in this scenario to achieve coordination and synchronization between threads. In asynchronous mode, each thread can run independently without coordination and synchronization, allowing threads to be started, paused, and terminated as needed to achieve more flexible program control. For instance, in multithreaded network programming, a separate thread can be launched for each client connection to handle data exchange independently, without mutual interference, effectively increasing program concurrency and response speed.

6.2 Synchronizing Threads using POSIX Multithreading API Functions

For multithreaded programs, synchronization refers to allowing only one thread to access a specific resource within a certain time period. During this time, other threads are not allowed to access the resource. This can be achieved by using mechanisms such as mutex, condition variables, reader-writer locks, and semaphores to synchronize resources.

6.2.1 Mutex

Mutex is a mechanism for thread synchronization, used to protect shared resources from the impact of concurrent access. Mutex allows only one thread to access shared resources, and other threads must wait until the mutex is released before they can access. The process of obtaining and releasing a lock is also referred to as locking and unlocking.

In POSIX systems, mutex is implemented through the pthread library. The following are several commonly used mutex functions: [Here, the specific functions related to mutex would be mentioned.

  1. The function used to initialize a mutex is pthread_mutex_init(), and its function declaration is as follows:

    #include <pthread.h>

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    • Parameter description:
      • mutex: A pointer to the mutex
      • attr: A pointer to the mutex's attributes; it can be set to NULL
    • Return value: Returns 0 on success, and an error code on failure.
  2. After thread initialization, the mutex can be locked using the function pthread_mutex_lock(), and its function declaration is as follows:

    #include <pthread.h>

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    • Parameter description:
      • mutex: A pointer to the mutex.
    • Return value: Returns 0 on success, and an error code on failure.
  3. Locking and unlocking operations must appear in pairs; otherwise, it may lead to deadlocks or other issues. The function used to unlock the mutex is pthread_mutex_unlock(), and its function declaration is as follows:

    #include <pthread.h>

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    • Parameter description:
      • mutex: A pointer to the mutex.
    • Return value: Returns 0 on success, and an error code on failure.
  4. When the mutex is no longer needed, it should be destroyed using the function pthread_mutex_destroy(), and its function declaration is as follows:

    #include <pthread.h>

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    • Parameter description:
      • mutex: A pointer to the mutex.
    • Return value: Returns 0 on success, and an error code on failure.

Example of multi-threaded accumulation before using a mutex:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>

int n = 0;

pthread_mutex_t mutex;

void *thread_1(void *arg)
{
int j;
for (j = 0; j < 1000000; j++) {
n++;
}
pthread_exit((void *)0);
}

void *thread_2(void *arg)
{
int j;
for (j = 0; j < 1000000; j++) {
n++;
}
pthread_exit((void *)0);
}
int main(void)
{
int j,err;
pthread_t th1, th2;

for (j = 0; j < 10; j++)
{
err = pthread_create(&th1, NULL, thread_1, (void *)0);
if (err != 0) {
printf("create new thread error:%s\n", strerror(err));
exit(0);
}
err = pthread_create(&th2, NULL, thread_2, (void *)0);
if (err != 0) {
printf("create new thread error:%s\n", strerror(err));
exit(0);
}

err = pthread_join(th1, NULL);
if (err != 0) {
printf("wait thread done error:%s\n", strerror(err));
exit(1);
}
err = pthread_join(th2, NULL);
if (err != 0) {
printf("wait thread done error:%s\n", strerror(err));
exit(1);
}
printf("n=%d\n", n);
n = 0;
}

return 0;
}

Example of multi-threaded accumulation after using a mutex:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>

int n = 0;

pthread_mutex_t mutex;

void *thread_1(void *arg)
{
int j;
for (j = 0; j < 1000000; j++)
{
pthread_mutex_lock(&mutex);
n++;
pthread_mutex_unlock(&mutex);
}
pthread_exit((void *)0);
}

void *thread_2(void *arg)
{
int j;
for (j = 0; j < 1000000; j++)
{
pthread_mutex_lock(&mutex);
n++;
pthread_mutex_unlock(&mutex);
}
pthread_exit((void *)0);
}
int main(void)
{
int j,err;
pthread_t th1, th2;

pthread_mutex_init(&mutex, NULL); // Initialize the mutex
for (j = 0; j < 10; j++)
{
err = pthread_create(&th1, NULL, thread_1, (void *)0);
if (err != 0) {
printf("create new thread error:%s\n", strerror(err));
exit(0);
}
err = pthread_create(&th2, NULL, thread_2, (void *)0);
if (err != 0) {
printf("create new thread error:%s\n", strerror(err));
exit(0);
}

err = pthread_join(th1, NULL);
if (err != 0) {
printf("wait thread done error:%s\n", strerror(err));
exit(1);
}
err = pthread_join(th2, NULL);
if (err != 0) {
printf("wait thread done error:%s\n", strerror(err));
exit(1);
}
printf("n=%d\n", n);
n = 0;
}
pthread_mutex_destroy(&mutex); // Destroy the mutex

return 0;
}
  • The above two programs create two threads, each incrementing n one million times. The expected final output should be 2000000. However, due to resource contention between threads without using a mutex, the output of the program without a mutex will not accumulate to 2000000.

6.2.2 Read-Write Lock

The read-write lock is a special type of lock that improves the efficiency of read operations on shared resources in concurrent programs. It consists of two types of locks: read lock and write lock. Multiple threads can hold the read lock simultaneously, but the write lock can only be held by a single thread. When a thread holds the write lock, other threads cannot acquire either the read or write lock, ensuring data consistency. Read-write locks are suitable for scenarios with frequent reads and infrequent writes, as they allow multiple threads to read from the shared resource simultaneously without blocking the program's performance.

  1. In the POSIX thread library, the function used to initialize a read-write lock is pthread_rwlock_init(), with the following function signature:

    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)
    • Parameters:
      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be initialized as a read-write lock
      • attr: A pointer to the pthread_rwlockattr_t structure representing the attributes of the read-write lock. If this parameter is NULL, default attributes are used
    • Return Value: Returns 0 if the function executes successfully, otherwise returns an error code
  2. Locking the read-write lock can be achieved in two modes: read mode and write mode, with the following function signatures:

    #include <pthread.h>
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
    • Parameters:

      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be locked in read mode.
    • Return Value: Returns 0 if obtaining the read lock succeeds, otherwise returns an error code.

      #include <pthread.h>
      int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
    • Parameters:

      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be locked in read mode.
    • Return Value: Returns 0 if obtaining the read lock succeeds, otherwise returns an error code.

    • When using pthread_rwlock_tryrdlock(), if another thread already holds the write lock (exclusive lock), the current thread's attempt to acquire the read lock will fail, and the function will immediately return failure.

      #include <pthread.h>
      int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
    • Parameters:

      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be locked in write mode.
    • After the successful execution of this function, the current thread will hold the write lock (exclusive lock), and other threads will be unable to hold either the read or write lock until the current thread releases the write lock.

    • Return Value: Returns 0 if obtaining the write lock succeeds, otherwise returns an error code.

      #include <pthread.h>
      int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)
    • Parameters:

      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be locked in write mode
    • After the successful execution of this function, the current thread will hold the write lock (exclusive lock), and other threads will be unable to hold either the read or write lock until the current thread releases the write lock.

    • The pthread_rwlock_trywrlock() function is used to attempt to acquire the write lock (exclusive lock) of the read-write lock. If the current read-write lock is already held by a read lock or a write lock, the function will immediately return failure. The parameter meanings are as follows:

    • Return Value: Returns 0 if obtaining the write lock succeeds, otherwise returns an error code.

  3. After the thread exits the critical section, it needs to unlock the read-write lock using the pthread_rwlock_unlock() function, with the following function signature:

    #include <pthread.h>
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
    • Parameters:
      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be unlocked.
    • After successful execution of this function, the current thread will release the read lock or write lock, and other threads will be able to hold the read or write lock.
  4. When the read-write lock is no longer needed, it should be destroyed using the pthread_rwlock_destroy() function, with the following function signature:

    #include <pthread.h>
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
    • Parameters:
      • rwlock: A pointer to the pthread_rwlock_t structure that needs to be destroyed
    • After successful execution of this function, the read-write lock will be destroyed, releasing the associated resources
  5. Simulating a read-write process for a shared variable:

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    #define NUM_THREADS 3
    #define NUM_ITERATIONS 10

    int shared_data = 0; // Shared data
    pthread_rwlock_t rwlock; // Read-write lock

    void *reader(void *arg)
    {
    for (int i = 0; i < NUM_ITERATIONS; ++i)
    {
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld: read shared_data = %d\n", (long int) arg, shared_data);
    pthread_rwlock_unlock(&rwlock);
    usleep(rand() % 100000); // Simulate read operation time
    }
    pthread_exit(NULL);
    }

    void *writer(void *arg)
    {
    for (int i = 0; i < NUM_ITERATIONS; ++i)
    {
    pthread_rwlock_wrlock(&rwlock);
    shared_data++;
    printf("Writer %ld: write shared_data = %d\n", (long int) arg, shared_data);
    pthread_rwlock_unlock(&rwlock);
    usleep(rand() % 100000); // Simulate write operation time
    }
    pthread_exit(NULL);
    }

    int main() {
    pthread_t threads[NUM_THREADS];
    pthread_rwlock_init(&rwlock, NULL);

    // Create reader threads and a writer thread
    for (intptr_t i = 0; i < NUM_THREADS; ++i) {
    if (i == 0) {
    pthread_create(&threads[i], NULL, writer, (void *) i);
    } else {
    pthread_create(&threads[i], NULL, reader, (void *) i);
    }
    }

    // Wait for threads to finish
    for (int i = 0; i < NUM_THREADS; ++i) {
    pthread_join(threads[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);
    return 0;
    }

6.2.3 Condition Variables

Condition Variables are a mechanism used for thread synchronization. They allow threads to wait for a specific condition to occur. When the condition is not met, the thread is blocked until another thread signals the occurrence of that condition, allowing the blocked thread to continue its execution. Condition Variables are often used in conjunction with Mutexes. When a thread needs to wait for a certain condition to be satisfied, it first acquires a Mutex, checks if the condition is met, and if not, enters a waiting state using the Condition Variable. When the condition changes, the waiting thread is notified and can proceed.

Condition Variables consist of two main operations: wait and signal. The wait operation is used to wait for a signal, and if the condition is not satisfied, the thread waits on the Condition Variable. The signal operation is used to send a signal, notifying waiting threads that the condition has been satisfied, and they can continue executing. When using Condition Variables, it is essential to use them in conjunction with Mutexes. When a waiting thread receives a signal, it reacquires the Mutex, checks if the condition is satisfied, and then continues its execution. Therefore, the usage of Condition Variables generally involves the following steps:

1). Initialize the Condition Variable and Mutex. 2). In the waiting thread, acquire the Mutex, check if the condition is satisfied, and if not, wait on the Condition Variable. 3). In the signaling thread, acquire the Mutex, change the condition's state, and then signal the waiting thread using the Condition Variable. 4). After receiving the signal, the waiting thread reacquires the Mutex, checks if the condition is satisfied, and continues execution if it is.

It is essential to note that when using Condition Variables, the Mutex must be acquired before waiting or signaling on the Condition Variable; otherwise, it can lead to a race condition. Additionally, when using Condition Variables, it is necessary to handle the situation of spurious wakeups, where threads waiting on the Condition Variable may be awakened unexpectedly. Therefore, the waiting thread should recheck the condition after being awakened.

  1. Condition Variables can be created and initialized using two methods: static initialization and function initialization.

    • Static initialization is suitable for situations where the Condition Variable is initialized at the time of definition. It uses the PTHREAD_COND_INITIALIZER macro to initialize a Condition Variable, which generates a static variable of type pthread_cond_t and initializes it to default values. The code example is as follows:

      #include <pthread.h>
      int pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    • Function initialization is used for dynamically creating Condition Variables during runtime. It requires using the pthread_cond_init function for initialization, which has the following prototype:

      #include <pthread.h>
      int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    • The cond parameter is a pointer to the Condition Variable object, and the attr parameter is a pointer to the attributes associated with the Condition Variable, typically set to NULL.

  2. Waiting on the Condition Variable: When using Condition Variables, a thread often needs to wait for a certain condition to be satisfied before proceeding. This can be achieved using the pthread_cond_wait() function to wait for a signal on the Condition Variable. The function declaration is as follows:

    #include <pthread.h>
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    This function will block the current thread until another thread calls pthread_cond_signal or pthread_cond_broadcast to notify the Condition Variable. Before waiting, the thread must acquire the mutex lock and release it during the waiting process, allowing other threads to modify shared states. When the wait is over, the function will automatically reacquire the mutex lock and return. At this point, the thread can check if the condition is satisfied and continue execution. It's essential to note that the pthread_cond_wait function can encounter spurious wakeups, which means it may return even if the Condition Variable was not signaled.

    Therefore, it is generally advised to put the waiting process in a loop and check if the condition is satisfied to avoid errors caused by spurious wakeups. Additionally, before using the pthread_cond_wait function, make sure the mutex lock is already acquired; otherwise, it will result in undefined behavior. Thus, it is recommended to use the following pattern when waiting on the Condition Variable:

    pthread_mutex_lock(&mutex);
    while (!condition_is_satisfied())
    {
    pthread_cond_wait(&cond, &mutex);
    }
    // Here, the condition is satisfied, and the thread can continue execution
    pthread_mutex_unlock(&mutex);
  3. Signaling waiting threads: To wake up waiting threads on a Condition Variable, you can use the pthread_cond_signal() or pthread_cond_broadcast() function. Their declarations are as follows:

    #include <pthread.h>
    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond);
    • Parameters: cond is a pointer to the Condition Variable object.
    • Return Value: Returns 0 on success and a positive integer error code on failure.
    • Note: Both functions require obtaining the corresponding mutex lock to avoid race conditions. The pthread_cond_signal function wakes up one waiting thread on the Condition Variable (if there is any), while pthread_cond_broadcast wakes up all waiting threads on the Condition Variable.
  4. Destroying the Condition Variable: The pthread_cond_destroy function is used to destroy a Condition Variable. Its declaration is as follows:

    #include <pthread.h>
    int pthread_cond_destroy(pthread_cond_t *cond);
    • Destroying the Condition Variable: The pthread_cond_destroy function is used to destroy a Condition Variable. Its declaration is as follows:
    • This function is used to release the resources associated with the Condition Variable. After using a pthread_cond_t type Condition Variable, it should be destroyed when no longer needed. Before destroying the Condition Variable, ensure that all threads associated with it have exited and are no longer using the Condition Variable.
  5. Find integers from 1 to 20 that are divisible by 3:

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // Initialize the mutex
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // Initialize the condition variable

    void *thread1(void *);
    void *thread2(void *);

    int i = 1;

    int main(void)
    {
    pthread_t t_a;
    pthread_t t_b;

    pthread_create(&t_a, NULL, thread2, (void *)NULL); // Create thread t_a
    pthread_create(&t_b, NULL, thread1, (void *)NULL); // Create thread t_b
    pthread_join(t_b, NULL); // Wait for thread t_b to finish
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    exit(0);
    }

    void *thread1(void *junk)
    {
    for (i = 1; i <= 20; i++)
    {
    pthread_mutex_lock(&mutex); // Lock the mutex
    if (i % 3 == 0)
    pthread_cond_signal(&cond); // Signal waiting threads on the condition variable
    else
    printf("thead1:%d\n", i); // Print numbers that are not divisible by 3
    pthread_mutex_unlock(&mutex); // Unlock the mutex

    sleep(1);
    }
    }

    void *thread2(void *junk)
    {
    while (i < 20)
    {
    pthread_mutex_lock(&mutex);

    if (i % 3 != 0)
    pthread_cond_wait(&cond, &mutex); // Wait on the condition variable
    printf("------------thread2:%d\n", i); // Print numbers that are divisible by 3
    pthread_mutex_unlock(&mutex);

    sleep(1);
    i++;
    }
    }
  • Compile and run the program:

    linaro@linaro-alip:~/Linux/thread$ gcc -o Condition_Variable Condition_Variable.c -lpthread
    linaro@linaro-alip:~/Linux/thread$ ./Condition_Variable
    thead1:1
    thead1:2
    ------------thread2:3
    thead1:4
    ------------thread2:6
    thead1:7
    ------------thread2:9
    thead1:10
    ------------thread2:12
    thead1:13
    ------------thread2:15
    thead1:16
    ------------thread2:18
    thead1:19
    linaro@linaro-alip:~/Linux/thread$

6.2.4 Semaphores

POSIX semaphores are mechanisms used for thread synchronization and mutual exclusion. They provide a set of functions and data types to implement operations for synchronizing and mutually accessing shared resources. There are two types of semaphores: named and unnamed.

  • Named Semaphores: Suitable for communication and synchronization between different processes. They are identified by a unique name in the system, and multiple processes can access the same named semaphore.
  • Unnamed Semaphores: Suitable for synchronization between threads within the same process. Unnamed semaphores can only be used in the context of shared memory, such as implementing mutual exclusion and synchronization between threads within a process. Hence, they are also referred to as memory-based semaphores.

Differences between POSIX and System V semaphores:

  • For POSIX, a semaphore is a non-negative integer commonly used for thread synchronization. In contrast, System V semaphores are a collection of one or more semaphores, corresponding to a semaphore structure designed for System V IPC services, and semaphores are just a part of it, mainly used for interprocess synchronization.
  • The header file for using POSIX semaphores is "<semaphore.h>", while for System V semaphores, it is "<sys/sem.h>".
  • In terms of usage, System V semaphores are more complex, while POSIX semaphores are relatively easier to use.
Unnamed Semaphores

To initialize an unnamed semaphore, use the sem_init() function with the following declaration:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • Parameter explanation:

    • sem: Pointer to the unnamed semaphore.
    • pshared: Specify if the semaphore is shared between processes (non-zero) or between threads (0).
    • value: Specify the initial value of the semaphore's counter.
  • Return value: Returns 0 on success, and -1 on failure.

To destroy an unnamed semaphore, use the sem_destroy() function with the following declaration:

#include <semaphore.h>

int sem_destroy(sem_t *sem);
  • Parameter explanation:

    • sem: Pointer to the unnamed semaphore
  • Return value: Returns 0 on success, and -1 on failure.

To wait for an unnamed semaphore, i.e., if the semaphore's counter is greater than 0, decrement it. If the counter's value is 0, block the thread or process until another thread or process increments the semaphore's counter, use the sem_wait() function with the following declaration:

#include <semaphore.h>

int sem_wait(sem_t *sem);
  • Parameter explanation: sem: Pointer to the unnamed semaphore.

  • Return value: Returns 0 on success, and -1 on failure.

To increment the counter of an unnamed semaphore and wake up threads or processes waiting on it, use the sem_post() function with the following declaration:

#include <semaphore.h>

int sem_post(sem_t *sem);
  • Parameter explanation: sem: Pointer to the unnamed semaphore.

  • Return value: Returns 0 on success, and -1 on failure.

Example of using unnamed semaphores:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define NUM_THREADS 3

sem_t semaphore;

void* threadFunction(void* arg)
{
int threadId = *((int*)arg);
printf("Thread %d is waiting.\n", threadId);
sem_wait(&semaphore); // Wait for the semaphore, if its value is greater than 0, continue execution; otherwise, block

printf("Thread %d acquired the semaphore.\n", threadId);
// Simulate thread's work
printf("Thread %d is working...\n", threadId);
sleep(2);

printf("Thread %d released the semaphore.\n", threadId);
sem_post(&semaphore); // Signal, increase the value of the semaphore

pthread_exit(NULL);
}

int main()
{
pthread_t threads[NUM_THREADS];
int threadIds[NUM_THREADS];

sem_init(&semaphore, 0, 1); // Initialize the semaphore with an initial value of 1

for (int i = 0; i < NUM_THREADS; i++)
{
threadIds[i] = i;
pthread_create(&threads[i], NULL, threadFunction, &threadIds[i]);
}

for (int i = 0; i < NUM_THREADS; i++)
{
pthread_join(threads[i], NULL);
}

sem_destroy(&semaphore); // Destroy the semaphore

return 0;
}
  • In this example program, three threads run concurrently, and they achieve mutual exclusion to a critical resource using an unnamed semaphore. Each thread waits to acquire the semaphore, performs some work, and then releases the semaphore, allowing other threads to acquire it. The initial value of the unnamed semaphore is set to 1, meaning only one thread can acquire the semaphore at a time, and other threads will have to wait. This ensures synchronization and mutual exclusion among the threads' access to the critical resource.

After compiling and running the program, the output is as follows:

linaro@linaro-alip:~/Linux/thread$ gcc -o posix_sem posix_sem.c -lpthread
linaro@linaro-alip:~/Linux/thread$ ./posix_sem
Thread 1 is waiting.
Thread 1 acquired the semaphore.
Thread 1 is working...
Thread 0 is waiting.
Thread 2 is waiting.
Thread 1 released the semaphore.
Thread 0 acquired the semaphore.
Thread 0 is working...
Thread 0 released the semaphore.
Thread 2 acquired the semaphore.
Thread 2 is working...
Thread 2 released the semaphore.
linaro@linaro-alip:~/Linux/thread$
Named Semaphores

To open or create a named semaphore, use the sem_open() function, with the following declaration:

#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
  • Parameters:

    • name: Specifies the name of the semaphore.
    • oflag: Specifies the flags for opening or creating the semaphore.
    • mode: Specifies the permission mask when creating a new semaphore.
    • value: Specifies the initial value of the semaphore counter.
  • Return Value: On success, the function returns a pointer to the named semaphore. On failure, it returns SEM_FAILED.

To close a named semaphore, use the sem_close() function, with the following declaration:

#include <semaphore.h>

int sem_close(sem_t *sem);
  • Parameters: sem: A pointer to the named semaphore.

  • Return Value: On success, the function returns 0. On failure, it returns -1.

To remove the semaphore from the system after all processes have closed it, use the sem_unlink() function, with the following declaration:

#include <semaphore.h>

int sem_unlink(const char *name);

Example of using named semaphores:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
sem_t* semaphore;
const char* semaphoreName = "/my_named_semaphore";
int childPid;

// Create the named semaphore
semaphore = sem_open(semaphoreName, O_CREAT | O_EXCL, 0644, 1);
if (semaphore == SEM_FAILED)
{
perror("sem_open");
exit(EXIT_FAILURE);
}

// Create a child process
childPid = fork();
if (childPid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}

if (childPid == 0)
{
// Child process

// Open the existing named semaphore
semaphore = sem_open(semaphoreName, 0);
if (semaphore == SEM_FAILED)
{
perror("sem_open");
exit(EXIT_FAILURE);
}

// Acquire the semaphore
printf("Child process is waiting.\n");
sem_wait(semaphore);

// Perform critical section code
printf("Child process acquired the semaphore.\n");
printf("Child process is working...\n");
sleep(2);

// Release the semaphore
printf("Child process released the semaphore.\n");
sem_post(semaphore);

// Close the semaphore
sem_close(semaphore);

exit(EXIT_SUCCESS);
}
else
{
// Parent process

// Acquire the semaphore
printf("Parent process is waiting.\n");
sem_wait(semaphore);

// Perform critical section code
printf("Parent process acquired the semaphore.\n");
printf("Parent process is working...\n");
sleep(2);

// Release the semaphore
printf("Parent process released the semaphore.\n");
sem_post(semaphore);

// Wait for the child process to finish
wait(NULL);

// Close and unlink the semaphore
sem_close(semaphore);
sem_unlink(semaphoreName);
}

return 0;
}
  • In this example program, the parent and child processes share a named semaphore. The parent process creates the named semaphore, acquires it, performs some work, and then releases it. After that, the parent process creates a child process, and the child process opens the existing named semaphore, acquires it, performs some work, and then releases it. By using a named semaphore, the parent and child processes achieve synchronization and mutual exclusion to the critical resource.

After compiling and running the program, the output is as follows:

linaro@linaro-alip:~/Linux/thread$ gcc -o posix_name_sem posix_name_sem.c -lpthread
linaro@linaro-alip:~/Linux/thread$ ./posix_name_sem
Parent process is waiting.
Parent process acquired the semaphore.
Parent process is working...
Child process is waiting.
Parent process released the semaphore.
Child process acquired the semaphore.
Child process is working...
Child process released the semaphore.
linaro@linaro-alip:~/Linux/thread$